/**
 * 
 * 链接
 *      OpenCV.js 官方文档 https://docs.opencv.org/4.5.5/d0/d84/tutorial_js_usage.html
 *      OpenCV.js 官方文档 基础操作(cv.Mat,cv.matFromArray,cv.matFromImageData,src.convertTo(dst, rtype),cv.MatVector,cv.imread,cv.imshow,cv.split,cv.merge,cv.Scalar,cv.copyMakeBorder, Mat实例方法(copyTo,clone,roi,ucharAt,ucharPtr,delete)) https://docs.opencv.org/4.5.5/de/d06/tutorial_js_basic_ops.html
 *      OpenCV.js 官方文档 图像的算术运算（加，减，位运算<AND, OR, NOT and XOR> cv.add,cv.subtract,cv.bitwise_and, cv.bitwise_not） https://docs.opencv.org/4.5.5/dd/d4d/tutorial_js_image_arithmetics.html
 *      OpenCV.js 官方文档 数据结构(Point, Scalar, Size, Circle, Rect, RotatedRect) https://docs.opencv.org/4.5.5/d5/df1/tutorial_js_some_data_structures.html
 *      OpenCV.js 官方文档  cv.resize, cv.warpAffine, cv.getAffineTransform and cv.warpPerspective https://docs.opencv.org/4.5.5/dd/d52/tutorial_js_geometric_transformations.html
 *      OpenCV.js 官方文档 显示图像(有cv.imread参数介绍) https://docs.opencv.org/4.5.5/df/d24/tutorial_js_image_display.html
 *      OpenCV.js 官方文档 cvtColor inRange https://docs.opencv.org/4.5.5/db/d64/tutorial_js_colorspaces.html
 *      OpenCV.js 官方文档 Image Threshold https://docs.opencv.org/4.5.5/d7/dd0/tutorial_js_thresholding.html
 *      OpenCV.js 官方文档 模板匹配 https://docs.opencv.org/4.5.5/d8/dd1/tutorial_js_template_matching.html 
 *      OpenCV.js 官方文档 watershed https://docs.opencv.org/4.5.5/d7/d1c/tutorial_js_watershed.html
 *      
 * 
 * 
 * 
 *      opencv 在图片上添加文字 https://blog.csdn.net/qq_41962968/article/details/122870539
 * 
 *      OpenCV.js快速入门指南(博文) https://blog.csdn.net/jm_12138/article/details/122910737
 * 
 * 
 *      opencv中的模板匹配--自适应目标匹配 https://blog.csdn.net/qq_46418503/article/details/119675943
 */

/*
 * 遇到问题解决(!!!可能不是正确的解决方法)
 * 1.opencv.js加载慢，下载到本地使用
 * 2.图片加载跨域问题，使用live server插件运行html文件
 * 3.cv.Mat is not a constructor  
        function onOpenCvReady() {
            cv['onRuntimeInitialized']=()=>{
                // do all your work here
                。。。
            }
        }
 *  4. Promise.all加载完图片后，再使用cv
 *  5. new cv.Size(col, row)
 *  !!!6. new cv.Rect(matchLoc, point) 得到的rect width，height undefined?? 直接自己写一个Rect_<_Tp>::Rect_
 *  !!!7.opencv.js 不支持 Mat Mat::operator()( const Rect& roi ) const用 Mat.roi(rect)代替 如：let ROI_img = img.roi(img_ROI);
 *  8. cv.resize 1,2参数类型检查 不能是undefined,使用变量时初始化 如： let matDst1 = new cv.Mat();
 *  9. cv.CV_BGR2GRAY变量废弃用 cv.COLOR_BGR2GRAY
 *  !!!10. opencv.js不支持 dct 使用 cv.dft https://github.com/opencv/opencv/issues/22383
 *  !!!11. 强制类型转换matDst2.ptr < uchar > (i),opencv.js找到个类似的  Mat.ptr(i) 如：let data1 = matDst1.ucharPtr(i);
 *  !!!12. 模拟数据结果就一个20符合 ，用<= ; pHash(ROI_img, templ) <= 20
 *  13. 模板canvas尺寸影响是否能查到结果
 *  14. 有多个结果时，取iDiffNum最小的那个
 *  15. 放大模板图片，i从负数开始如-10
 *  16. 拿到显示结果(就裁剪的那个范围)图片到canvas
 *  17. onRuntimeInitialized 内代码错误 无法触发事件，控制台手动cv.onRuntimeInitialized()
 *  18. Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true.
            var ctx1 = canvas1.getContext('2d', {
                    willReadFrequently: true,
                });
            https://github.com/niklasvh/html2canvas/issues/2986
 *  19. roi 不能直接用作Mat，得使用clone一下 如: scaleSearchResult1.ROI_img_s.clone()
 *  20. 直接用模板查询找不到数字，可能是数字图片太小的原因。inRange 二值化图片    
 *  21. 由于文字，数字的影响，rune匹配结果不符合预期（明明有DOL，但识别成了ZER）,一样使用二值化  
 *  22. 代码注释
 *      js@param使用 https://www.shouce.ren/api/view/a/13289
 *      js@typedef使用 https://www.shouce.ren/api/view/a/13306
 *      js@type使用 https://www.shouce.ren/api/view/a/13305
 *  23.内存泄漏
 *      Mat没有delete
 *  24.opencv判断矩形相交 https://blog.csdn.net/kezunhai/article/details/8830882
 *  25.误识别Rune问题 用mmCompare函数，比较重叠图（误识图），取最大相似度的图片
 *          https://blog.csdn.net/m0_71741835/article/details/127937354
 *  26. 同上 缩放模板匹配+mmCompare相似度比较
 *  27. opencv_4.5.5.js:1 Uncaught 6704400
 *          缩放模板匹配时，模板尺寸比原图尺寸大，使用前要比较一下或者缩放比例范围控制  
 * 
 *  
 */


/**  
 * 待优化
 * @todo 循环缩放模板图片的次数是20，博文是10；如何从次数角度优化， iDiffNum为0直接跳出循环
 * @todo 程序执行时间太长了。
 *          screenshot1不开调试  32s  开调试    11.4s 
 *          screenshot2不开调试  62s  开调试    19s
 * @todo opencv.js文件太大了， 直接应用中下载
 * @todo rune误识排查后还是有可能有错误的信息，rune识别成背包区域外的物体、识别成背包内的其他物品（如 scroll,potion）
 *              用截图，指定识别背包区域<如果裁剪图片的话，记得考虑缩放比率和缩放次数>，使用前将背包其他物品清除，加入scroll,potion 识别功能
 * @done 表格显示列样式宽度最好用class,用nth-of-type 调整顺序时改的好烦呀
 * 
 *      
 *              
 *          
 *   
 * 
 *  思考
 *@todo  案例 模板图片与待搜索图片一致，或比它大，那有没有比它小的时候？？可以缩小，那也可以放大，但不能比待搜索条件大
 *          i 正 缩小， i 负放大
 *@todo  pHash 函数原理
 *@todo  !!!inRange 参数原理（我的解决方法是，ps 滴管工具 + 瞎调） 
 *@todo  watershed研究
 *@todo  grabcut 是否可以使用智能抠图 https://docs.opencv.org/4.5.5/dd/dfc/tutorial_js_grabcut.html
 * 
 * 
 *   
 */

/**
 * @todo #7367 !!! 不同分辨率导入问题整理
 *          html canvas 尺寸动态自适应
 *          缩放模板匹配（只管背包区域和符石，数字是轮廓匹配）优化
 *              计算不同分辨率 符石 预估大小（注意：模板匹配是贴边匹配，图片素材有留白，）
 *              注意：screenshot2 中 BOR符石 识别成 ODO,虽然可以人工确认
 *          数字轮廓匹配，轮廓序号如何确定？
 *              轮廓序号问题，如果上一步让模板贴边匹配，那这一步序号就固定了？
 *              所有轮廓都匹配一遍，再比较？？
 * 
 * 
 * 
 * @todo 应用使用流程
 *          截图识别
 *              获取截图
 *                  直接webview获取本机图片
 *                  webview获取但app中传过来的图片
 *              识别rune数量关系
 *              结果展示
 *              人工校对
 *              结果
 *          人工录入
 *              截图应用内参考
 *              切应用查看
 * @todo cavas动态创建
 * @todo 动态计算缩放比例值，合理设置缩放次数
 *          需要游戏数据，如背包区域不同手机的缩放规则（而且我这个应用也要考虑不同手机分辨率问题）
 * @todo 用到的cv代码注释，cv命名空间，cv.Mat, cv.Mat.roi() ,cv.Point 等
 * @done 测试图片中 6 ZOB 数量匹配异常，解决思路 裁剪原图(只匹配右下角)/二值化剔除v  
 * @done 根据识别rune坐标中心点排序（从左往右，从上到下）
 * @done 数字轮廓识别、数字比较识别有一步错了，就直接退出了；加上try catch
 */


/**@constant */
const DEFAULT_D_RES = 100;

/**@constant */
const DEF_NUMS_AREA = [1, 4];// 默认识别数量范围（考虑背包中单个rune数量达到5的情况很少很少，可选值1~7，数字0,8模板要特殊处理暂不考虑）


/**@constant */
const TUNE_TOOLS_ISDEBUG = true;// 调试开关
// const TUNE_TOOLS_ISDEBUG = false;// 调试开关
/**@constant */
const DEBUG_BAG_I = 0;// screenshot1 调试用，上一次第一次成功模板匹配bag时i
// const DEBUG_BAG_I = -3;// screenshot2 调试用，上一次第一次成功模板匹配bag时i
/**@constant */
const DEBUG_RUNE_I = -9;// screenshot1 调试用，上一次第一次成功模板匹配rune时i
// const DEBUG_RUNE_I = -17;// screenshot2 调试用，上一次第一次成功模板匹配rune时i
/**@constant @deprecated */
const DEBUG_NUM_I = 4;// 调试用，上一次第一次成功模板匹配数字时i
/**@constant */
const DEBUG_RUNES_NUM = 20;// 调试用，尽量少设置校验RUNES总数 7:TRU与DOL误识  可选值 1~总数（20）


/**
 * @typedef {Object} Rune
 * @property {string} src   rune图片存储位置
 * @property {string} name  rune名称
 * @property {number} order rune序号
 */



/**
 * @typedef {Object} Num
 * @property {string} src   数字图片存储位置
 * @property {string} name   数字值字符串
 * @property {number} order 数字序号
 */


/**
 * @typedef {Object} Loc
 * @typedef {number} x
 * @typedef {number} y
 */


/**
 * @typedef {Object} ResultTableRowData
 * @typedef {number} order 序号
 * @typedef {string} canvas1 符石匹配结果展示用canvas id
 * @typedef {string} rune_name 符石名称
 * @typedef {string} rune_img 符石名称对应图片
 * @typedef {string} canvas2 数量匹配结果展示用canvas id
 * @typedef {string} num 数量匹配结果
 */

/**
 * @description 校验状态
 * @constant 
 * @readonly
 * @enum {number}
*/
const CHECK_STATE = {
    /** 初始状态 */
    INIT: 0,
    /** 正确 */
    TRUE: 1,
    /** 错误 */
    FALSE: 2,
};

/**
 * 
 * @typedef {Object} Rect
 * @property {number} x
 * @property {number} y
 * @property {number} width
 * @property {number} height
 */




/**
 * @description 
 * @typedef {Object} MatchedData              缩放模板匹配结果及一些其他数据
 * @property {string=}          name          map转成数组数据时，保存map的key
 * @property {Rect=}            rect          匹配rect
 * @property {cv.Mat=}          area_o        模板匹配成功区域(拿原图，非二值化）
 * @property {cv.Mat=}          templ         模板数据(二值化)
 * @property {cv.Mat=}          area          模板匹配成功区域数据(二值化）
 * @property {number}     [checkState = CHECK_STATE.INIT]    数据校验状态（Rune识别中剔除错误识别使用）
 * @property {Loc=}             matchLoc      识别区域左上角坐标，感觉Loc跟cv.Point数据没区别
 * @property {cv.Point=}        point         识别区域右下角坐标
 */


/**
 * @typedef {Object}        scaleSearchData                 scaleSearch函数形参data 数据类型
 * 
 * @property { true= }      defBag                          defBag, defNum, defRune三选一 值为true 识别背包区域
 * @property { true= }      defRune                         defBag, defNum, defRune三选一 值为true 识别rune
 * @deprecated @property { true= }      defNum              defBag, defNum, defRune三选一 值为true  识别数字；废弃，识别数字用轮廓匹配+mmCompare
 * @property { number= }    scale_matched_rune_sus_i_1st    第一次成功缩放匹配rune时的i值，用于遍历优化
 * @property { number= }    scale_matched_num_sus_i_1st     第一次成功缩放匹配数字时的i值，用于遍历优化
 * 
 * @todo defBag,defRune,defNum应该改成枚举类型
 * @todo 如何将类型中的属性scale_matched_num_sus_i_1st标记为弃用状态 ，直接用@deprecated没用
 */




/**
 * @description 
 * @typedef {Object}    scaleSearchResult           scaleSearch函数返回数据类型
 * @property {string}   iDiffNum_res                iDiffNum
 * @property {number}   size_res                    成功时匹配对应尺寸
 * @property {number}   const_i                     成功时匹配对应缩放循环i
 * @property {Loc}      match_rect_data_loc         匹配区域Loc,给_Rect方法使用
 * @property {cv.Point} match_rect_data_point       匹配区域Point,给_Rect方法使用
 */

/**
 * @description  opencv.js 加载触发 的onload 事件
 */
async function openCvReady() {


    /**
     * @constant
     * @type {Rune[]}
     */
    const RUNES_DATA = [{
        src: "./runes/rune_1_SIL.png",
        name: "SIL",
        order: 1
    },
    {
        src: "./runes/rune_2_ZER.png",
        name: "ZER",
        order: 2
    },
    {
        src: "./runes/rune_3_MON.png",
        name: "MON",
        order: 3
    },
    {
        src: "./runes/rune_4_KRA.png",
        name: "KRA",
        order: 4
    },
    {
        src: "./runes/rune_5_OGI.png",
        name: "OGI",
        order: 5
    },
    {
        src: "./runes/rune_6_DOL.png",
        name: "DOL",
        order: 6
    },
    {
        src: "./runes/rune_7_TRU.png",
        name: "TRU",
        order: 7
    },
    {
        src: "./runes/rune_8_CIM.png",
        name: "CIM",
        order: 8
    },
    {
        src: "./runes/rune_9_SIV.png",
        name: "SIV",
        order: 9
    },
    {
        src: "./runes/rune_10_KLY.png",
        name: "KLY",
        order: 10
    },
    {
        src: "./runes/rune_11_ZOB.png",
        name: "ZOB",
        order: 11
    },
    {
        src: "./runes/rune_12_BOR.png",
        name: "BOR",
        order: 12
    },
    {
        src: "./runes/rune_13_ODO.png",
        name: "ODO",
        order: 13
    },
    {
        src: "./runes/rune_14_ZOL.png",
        name: "ZOL",
        order: 14
    },
    {
        src: "./runes/rune_15_PYD.png",
        name: "PYD",
        order: 15
    },
    {
        src: "./runes/rune_16_SAK.png",
        name: "SAK",
        order: 16
    },
    {
        src: "./runes/rune_17_VIN.png",
        name: "VIN",
        order: 17
    },
    {
        src: "./runes/rune_18_DOS.png",
        name: "DOS",
        order: 18
    },
    {
        src: "./runes/rune_19_COL.png",
        name: "COL",
        order: 19
    },
    {
        src: "./runes/rune_20_COS.png",
        name: "COS",
        order: 20
    }
    ];

    /**
     * @constant
     * @type {Num[]}
     */
    const NUMS_DATA = [{
        src: "./nums/num_0.png",
        name: "0",
        order: 0
    }, {
        src: "./nums/num_1.png",
        name: "1",
        order: 1
    }, {
        src: "./nums/num_2.png",
        name: "2",
        order: 2
    }, {
        src: "./nums/num_3.png",
        name: "3",
        order: 3
    }, {
        src: "./nums/num_4.png",
        name: "4",
        order: 4
    }, {
        src: "./nums/num_5.png",
        name: "5",
        order: 5
    }, {
        src: "./nums/num_6.png",
        name: "6",
        order: 6
    }, {
        src: "./nums/num_7.png",
        name: "7",
        order: 7
    }, {
        src: "./nums/num_8.png",
        name: "8",
        order: 8
    }, {
        src: "./nums/num_9.png",
        name: "9",
        order: 9
    }]
    // 脚本加载完成
    // console.log(cv);
    // console.time("执行时间");
    console.log(1);

    var src1 = "./screenshot1.jpg";
    // var src1 = "./screenshot1_1.jpg";
    // var src1 = "./screenshot1_2.jpg";
    // var src1 = "./screenshot1_3.jpg";
    // var src1 = "./screenshot1_4.jpg";
    // var src1 = "./screenshot1_5.jpg";

    // var src1 = "./screenshot2.jpg";// screenshot2

    var src_bag = "./bag_border.png";


    /**
     * @type { Map<string, HTMLImageElement> }
     * ！！！注意保证 rune.name 唯一
     * key: rune_name value:img 加载的图片
     */
    let rune_imgs = new Map(); // 存放加载的runes图片
    /**
     * @type { Map<string, HTMLImageElement> }
     */
    let num_imgs = new Map(); // 存放加载的数字图片

    /**@type {Map<string,MatchedData>} */
    let runes_matched_result = new Map(); // 存放rune匹配数据

    await Promise.all([await loadImg(src1).then(img => {
        // console.log(img);
        /**
         * @todo 根据图片宽高调整canvas,不同的手机截图尺寸可能不一样
         * @todo 根据匹配结果设置canvas尺寸
         * @todo 当前数据识别进度展示
         * @todo 停止数据识别与恢复数据识别与重新数据识别 js线程 https://docs.opencv.org/4.5.5/d9/df5/tutorial_js_intelligent_scissors.html
         */


        let canvas = document.getElementById('imageCanvasInput');
        let ctx = canvas.getContext('2d', {
            willReadFrequently: true,
        });
        // console.log("导入的截图信息:", img.width, img.height);
        // console.log("canvas信息:", canvas.width, canvas.height);
        canvas.width = img.width;
        canvas.height = img.height;
        ctx.drawImage(img, 0, 0);


        let canvas_out = document.getElementById('screenshot_bag_marked_Canvas');
        let ctx_out = canvas_out.getContext('2d', {
            willReadFrequently: true,
        });
        canvas_out.width = img.width;
        canvas_out.height = img.height;
        ctx_out.drawImage(img, 0, 0);

        // console.log(12);
    }), await loadImg(src_bag).then(img => {
        // console.log(img);
        let canvas = document.getElementById('templateBagCanvasInput');
        var ctx = canvas.getContext('2d', {
            willReadFrequently: true,
        });
        ctx.drawImage(img, 0, 0);

        // console.log(12);
    }), ...RUNES_DATA.map(async (el) => {
        let src = el.src;
        let name = el.name;
        return (
            await loadImg(src).then(img => {
                // console.log(img);
                rune_imgs.set(name, img);
            })
        );
    }), ...NUMS_DATA.map(async (el) => {
        let src = el.src;
        let name = el.name;
        return (
            await loadImg(src).then(img => {
                num_imgs.set(name, img);
            })
        );
    })]);

    console.log("图片资源加载完成", rune_imgs, num_imgs);

    /**
     * opencv.js 在真正初始化之前加载并触发 onload 事件。为了等到 opencv.js 真正准备好，opencv.js 提供了一个挂机“onRuntimeInitialized”。像这样使用它
     */
    cv['onRuntimeInitialized'] = () => {
        // return;
        console.time("识别时间");
        console.log(`cv.onRuntimeInitialized()被调用执行`);
        // do all your work here


        // -----缩放模板匹配背包区域
        console.log(2);
        let src_screenshot = cv.imread('imageCanvasInput'); // 待查询
        let src_s_c = src_screenshot.clone();// clone一份

        /**
         * @type {MatchedData} 
         */
        let bag_matched_data = {};

        {
            // 添加块级作用域，防止变量污染

            let templ = cv.imread('templateBagCanvasInput'); // bag border模板

            let dst = new cv.Mat();
            let mask = new cv.Mat();

            // -------二值化待搜索图片与模板图片，提高缩放模板查询精准度
            let arr_l = [40, 40, 40, 255];
            let arr_h = [70, 70, 70, 255];

            // 二值化待搜索图片
            let src_b_w = new cv.Mat();// 用于保存二值化图片
            bwImage(src_s_c, arr_l, arr_h, src_b_w);
            /**
             * @todo inRange dst数据处理， imshow+imread 将dst数据类型从 cv.CV_8U转化成 cv.CV_8UC4，搞不懂为啥dst.convertTo(dst, cv.CV_8UC4);无效
             * 不转成这个cv.CV_8UC4格式，模板匹配保存
             * @todo 在那个canvas上面转化数据都一样，但要保证canvas尺寸一致，最好整个temp canvas上转化
             * */
            // 转化数据 cv.CV_8U->cv.CV_8UC4
            cv.imshow("imageCanvasInput", src_b_w);
            src_b_w = cv.imread("imageCanvasInput"); // 二值化图
            cv.imshow("imageCanvasInput", src_s_c); // 最后显示的还是正常的图片

            // 二值化模板图片
            let templ_b_w = new cv.Mat();// 用于保存二值化图片
            bwImage(templ, arr_l, arr_h, templ_b_w);

            // 转化数据 cv.CV_8U->cv.CV_8UC4
            cv.imshow("templateBagCanvasInput", templ_b_w);
            templ_b_w = cv.imread("templateBagCanvasInput"); // 二值化图
            cv.imshow("templateBagCanvasInput", templ); // 最后显示的还是正常的图片

            /** @type {scaleSearchData} */
            let scaleSearchData = {
                defBag: true, // 识别背包区域
                // defNum: true, // 识别数字
                // defRune: true, // 识别rune
            };

            let scaleSearchResult1 = scaleSearch(src_b_w, templ_b_w, dst, mask, scaleSearchData);


            if (scaleSearchResult1.iDiffNum_res == DEFAULT_D_RES) {
                console.warn(`背包区域没匹配到……`);
            } else {


                let matchLoc = scaleSearchResult1.match_rect_data_loc;
                let point = scaleSearchResult1.match_rect_data_point;


                let match_rect = _Rect(matchLoc, point);
                console.log(`识别背包区域结果:`, `i: ${scaleSearchResult1.const_i}`,
                    `iDiffNum: ${scaleSearchResult1.iDiffNum_res}`, scaleSearchResult1.size_res, match_rect);

                bag_matched_data.rect = match_rect;
                // bag_matched_data.templ = templ;// 模板数据

                bag_matched_data.area_o = src_screenshot.roi(match_rect);// 模板匹配成功区域(拿原图，非二值化，用克隆前的图片)
                bag_matched_data.templ = templ_b_w;// 模板数据(二值化)
                bag_matched_data.area = src_b_w.roi(match_rect);// 模板匹配成功区域(二值化)
                bag_matched_data.checkState = CHECK_STATE.INIT;//设置初始校验状态
                bag_matched_data.matchLoc = matchLoc;
                bag_matched_data.point = point;


            }



            // -----绘制矩形选框
            console.log(21);

            let color_1 = new cv.Scalar(0, 255, 0, 255);// 背包框颜色
            cv.rectangle(src_s_c, bag_matched_data.matchLoc, bag_matched_data.point, color_1, 2, cv.LINE_8, 0);
            cv.imshow('screenshot_bag_marked_Canvas', src_s_c);
            cv.imshow("bag_rune_marked_Canvas", bag_matched_data.area_o);

            src_b_w.delete();
            // templ_b_w.delete(); // MatchedData.templ引用，最下面清

            templ.delete();
            dst.delete();
            mask.delete();
        }


        // -----缩放模板匹配rune
        console.log(3);

        // let canvas_bag_rune_marked = document.getElementById('bag_rune_marked_Canvas');
        // let ctx_b_r_m = canvas_bag_rune_marked.getContext('2d', {
        //     willReadFrequently: true,
        // });
        let canvas_rune_templ = document.getElementById('templateCanvasInput');
        let ctx_c_r_t = canvas_rune_templ.getContext('2d', {
            willReadFrequently: true,
        });

        let src_bag = cv.imread('bag_rune_marked_Canvas'); // 背包选区，待查询
        let src_bag_c = src_bag.clone();

        let scale_matched_rune_sus_i_1st = undefined; // 记录第一次成功缩放匹配rune时的i值,减少计算量RUNES_DATA.length x (缩放次数) < 1 x (截止到第一次成功缩放匹配时的缩放次数次数) + (runes RUNES_DATA.length-1) x 1(固定缩放比列)

        for (let m = 0; m < RUNES_DATA.length; m++) {
            if (TUNE_TOOLS_ISDEBUG && m == DEBUG_RUNES_NUM) {
                break;
            }
            /**@type {MatchedData} */
            let runes_matched_data = {};

            let rune = RUNES_DATA[m];
            let img_rune = rune_imgs.get(rune.name);
            if (img_rune === undefined) {
                console.warn(`rune ${rune.name} 图片加载失败`);
                continue;
            }
            ctx_c_r_t.clearRect(0, 0, canvas_rune_templ.width, canvas_rune_templ.height); // 清空画布
            ctx_c_r_t.drawImage(img_rune, 0, 0);
            let templ = cv.imread('templateCanvasInput'); // Rune模板

            let dst = new cv.Mat();
            let mask = new cv.Mat();

            // -------二值化待搜索图片与模板图片，提高缩放模板查询精准度
            let arr_l = [140, 140, 140, 255];
            let arr_h = [200, 200, 200, 255];

            // 二值化待搜索图片
            let src_b_w = new cv.Mat();// 用于保存二值化图片
            bwImage(src_bag_c, arr_l, arr_h, src_b_w);

            // 转化数据 cv.CV_8U->cv.CV_8UC4
            cv.imshow("bag_rune_marked_Canvas", src_b_w);
            src_b_w = cv.imread("bag_rune_marked_Canvas"); // 二值化图
            cv.imshow("bag_rune_marked_Canvas", src_bag_c); // 最后显示的还是正常的图片

            // 二值化模板图片
            let templ_b_w = new cv.Mat();// 用于保存二值化图片
            bwImage(templ, arr_l, arr_h, templ_b_w);

            // 转化数据 cv.CV_8U->cv.CV_8UC4
            cv.imshow("templateCanvasInput", templ_b_w);
            templ_b_w = cv.imread("templateCanvasInput"); // 二值化图
            cv.imshow("templateCanvasInput", templ); // 最后显示的还是正常的图片

            /** @type {scaleSearchData} */
            let scaleSearchData = {
                // defNum: true, // 识别数字
                defRune: true, // 识别rune
            };
            if (scale_matched_rune_sus_i_1st !== undefined) {
                scaleSearchData["scale_matched_rune_sus_i_1st"] = scale_matched_rune_sus_i_1st;
            }
            let scaleSearchResult1 = scaleSearch(src_b_w, templ_b_w, dst, mask, scaleSearchData);


            if (scaleSearchResult1.iDiffNum_res == DEFAULT_D_RES) {
                console.warn(`Rune-${rune.name}没匹配到……`);
            } else {
                scale_matched_rune_sus_i_1st = scaleSearchResult1.const_i;


                let matchLoc = scaleSearchResult1.match_rect_data_loc;
                let point = scaleSearchResult1.match_rect_data_point;


                let match_rect = _Rect(matchLoc, point);
                console.log(`识别Rune-${rune.name}结果:`, `i: ${scaleSearchResult1.const_i}`,
                    `iDiffNum: ${scaleSearchResult1.iDiffNum_res}`, scaleSearchResult1.size_res, match_rect);

                // ------存储缩放模板匹配结果
                runes_matched_data.rect = match_rect;
                // runes_matched_data.templ = templ;// 模板数据
                runes_matched_data.area_o = src_bag.roi(match_rect);// 模板匹配成功区域(拿原图，非二值化，用克隆前的图片)
                runes_matched_data.templ = templ_b_w;// 模板数据(二值化)
                runes_matched_data.area = src_b_w.roi(match_rect);// 模板匹配成功区域(二值化)
                runes_matched_data.checkState = CHECK_STATE.INIT;//设置初始校验状态
                runes_matched_data.matchLoc = matchLoc;
                runes_matched_data.point = point;



                runes_matched_result.set(rune.name, runes_matched_data);


            }

            templ.delete();
            src_b_w.delete();
            // templ_b_w.delete(); // MatchedData.templ引用，最下面清
            dst.delete();
            mask.delete();

            // console.timeEnd("执行时间");
        }

        // return;


        // -----Rune缩放模板匹配结果遍历寻找识别错误的部分
        console.log(4);
        let runes_def_checked = [];// 处理完成的数据
        if (runes_matched_result.size > 0) {
            let arr1 = Array.from(runes_matched_result).map((el) => {
                let key = el[0];
                let val = el[1];
                val.name = key;
                return val
            });// map转化成数组方便操作

            while (arr1.length > 0) {
                // if (TUNE_TOOLS_ISDEBUG && rune.name === "TRU") {

                // 重叠检测，即误匹配定位
                let arr3 = [];// 存放可能误匹配的数据（里面肯定有一个是正确的，不考虑截图里面一个rune都没有的情况）

                let flag = false;
                let value1 = arr1[0];
                if (arr1.length == 1) {
                    value1.checkState = CHECK_STATE.TRUE;// 当就剩1个时，那这个肯定是正确的
                    let els = arr1.splice(0, 1);
                    runes_def_checked.push(els[0]);
                    break;
                }
                // 每次while循环 拿arr1[0] 与 其他元素重叠借此
                for (let m = 1; m < arr1.length; m++) {
                    let value2 = arr1[m];
                    // let rect = value1.rect;
                    let val = rrOverlap(value1.rect, value2.rect);
                    // 虽然正确模板匹配其他的重叠率都为0，但就怕有问题（观察到打印的数据rect.x,rect.y有时相差1），还是>0.2吧，
                    if (val > 0.2) {
                        console.log(`Rune-${value1.name}与Rune-${value2.name}识别重叠率: ${val}`);
                        arr3.push(value2);
                        flag = true;
                    }
                }


                if (!flag) {
                    // value1这个没有重叠的，是正确的
                    value1.checkState = CHECK_STATE.TRUE;
                    let els = arr1.splice(0, 1);
                    runes_def_checked.push(els[0]);
                    continue;
                }

                // 寻找正确的那个,排除错误的
                let max = 0;
                let max_m = 0;
                arr3.push(value1);
                for (let m = 0; m < arr3.length; m++) {
                    let el = arr3[m];
                    let val = mmCompare(el.templ, el.area);
                    if (val > max) {
                        max = val;
                        max_m = m;
                    }
                }

                for (let m = 0; m < arr3.length; m++) {
                    let el = arr3[m];
                    for (let n = 0; n < arr1.length; n++) {
                        let ele = arr1[n];
                        if (ele.name == el.name) {
                            if (max_m == m) {
                                ele.checkState = CHECK_STATE.TRUE;// 匹配度最高的是正确的
                                console.log(`Rune-${ele.name}匹配度最高: `, max);
                            } else {
                                ele.checkState = CHECK_STATE.FALSE;// 其他的可以排除是错误的
                            }
                            let els = arr1.splice(n, 1);
                            runes_def_checked.push(els[0]);
                            break;
                        }
                    }


                }
            }

            // console.log(runes_def_checked);
        }




        // -----绘制矩形选框
        console.log(5);
        let runes_def_fix_unsort = [];// 保存校验
        let runes_def_fix = [];// 保存校验
        let color_2 = new cv.Scalar(255, 0, 0, 255);// 符石框颜色
        let color_3 = new cv.Scalar(255, 0, 255, 255);// 序号颜色(洋红)
        let rune_nums = 0;

        let result_table_data = [];
        let rst_tb_dom = document.getElementById("result_table").children[1];// table > tbody 
        let tb_innerhtml = "";

        for (let m = 0; m < runes_def_checked.length; m++) {
            let ele = runes_def_checked[m];
            if (ele.checkState == CHECK_STATE.FALSE) {
                continue;// 排除的查询结果
            }
            if (ele.checkState == CHECK_STATE.INIT) {
                // 应该不可能走这个
                console.warn("上一步剔除识别操作异常:", m, runes_def_checked[m]);
                continue;
            }
            // console.log(matchLoc, point); //matchLoc 左上角 point右下角
            rune_nums++;
            runes_def_fix_unsort.push(ele);
            // runes_def_fix.push(ele);
        }
        // console.log("runes_def_fix_unsort:", runes_def_fix_unsort);


        // -----排序（matchLoc 左上角 ，同一行/列 允许误差）
        /**
         * 坐标轴
         *  ----->x
         *  |
         *  |
         * yv
         */

        // runes_def_fix = runes_def_fix_unsort.sort();// sort方法会改变runes_def_fix_unsort，并且返回的就是指向它本身
        // runes_def_fix = runes_def_fix_unsort;

        // 先从上往下排列，即y从小到大
        runes_def_fix_unsort.sort((a, b) => (a.matchLoc.y - b.matchLoc.y));// sort方法会改变runes_def_fix，并且返回的就是指向它本身
        console.log("runes_def_fix_unsort:", runes_def_fix_unsort);

        // 先从左往右排列，即x从小到大

        const runes_def_h = runes_def_fix_unsort[0].rect.height;

        if (runes_def_fix_unsort.length <= 1) {
            runes_def_fix = [...runes_def_fix_unsort];
        } else {
            let sort_index = 1;
            let runes_def_row = [runes_def_fix_unsort[0]];
            while (sort_index < runes_def_fix_unsort.length) {
                let ele = runes_def_fix_unsort[sort_index];
                let prev = runes_def_fix_unsort[sort_index - 1];
                if (Math.abs(ele.matchLoc.y - prev.matchLoc.y) < runes_def_h) {
                    // 处于同一行
                    runes_def_row.push(ele);
                    if (sort_index == (runes_def_fix_unsort.length - 1)) {
                        runes_def_row.sort((a, b) => (a.matchLoc.x - b.matchLoc.x));// 从左往右排列
                        runes_def_fix.splice(runes_def_fix.length, 0, ...runes_def_row);
                    }
                    sort_index++;
                } else {
                    // 另起一行
                    runes_def_row.sort((a, b) => (a.matchLoc.x - b.matchLoc.x));// 从左往右排列
                    runes_def_fix.splice(runes_def_fix.length, 0, ...runes_def_row);


                    if (sort_index == (runes_def_fix_unsort.length - 1)) {
                        sort_index++;
                        runes_def_fix.splice(runes_def_fix.length, 0, ele);// 它正好是最后一个，它自己独占一行
                    } else {
                        runes_def_row = [ele];
                        sort_index++;
                    }

                }
            }
        }


        console.log("runes_def_fix:", runes_def_fix);

        // ------生成表格、对应图片画框 画序号
        for (let m = 0; m < runes_def_fix.length; m++) {
            let ele = runes_def_fix[m];
            // console.log(matchLoc, point); //matchLoc 左上角 point右下角


            // ----- 结果表格行html初始化（得先创建canvas dom）
            let result_table_row_data = {
                /** 序号 */
                order: 0,
                /** 符石匹配结果展示用canvas id */
                canvas1: "",
                /** 符石名称 */
                rune_name: "",
                /** 符石名称对应图片 */
                rune_img: "",
                /** 数量匹配结果展示用canvas id */
                canvas2: "",
                /** 数量匹配结果 */
                num: "",
                /** 数量匹配测试展示用canvas id */
                canvas2_t: "",
            };
            let img_rune = rune_imgs.get(ele.name);
            if (img_rune === undefined) {
                console.warn(`ele.name ${ele.name} 异常`);
            }

            result_table_row_data.order = m + 1;
            result_table_row_data.canvas1 = "rune_matched_res_" + result_table_row_data.order;
            result_table_row_data.rune_name = ele.name || '?';
            result_table_row_data.rune_img = img_rune.src || '';
            result_table_row_data.canvas2 = "num_matched_res_" + result_table_row_data.order;

            result_table_row_data.canvas2_t = "num_matched_test_" + result_table_row_data.order;

            tb_innerhtml += `
            <tr>
                <td class="order">${result_table_row_data.order}</td>
                <td class="rune_name">${result_table_row_data.rune_name}</td>
                <td class="rune_img"><img src="${result_table_row_data.rune_img}" alt=""></td>
                <td class="canvas1"><canvas width="150" height="150" id="${result_table_row_data.canvas1}" class="rune_matched_result"></canvas></td>
                <td class="canvas2"><canvas width="150" height="150" id="${result_table_row_data.canvas2}" class="num_matched_result"></canvas></td>
                <td class="num">?</td>
                <td class="canvas2_t"><canvas width="150" height="150" id="${result_table_row_data.canvas2_t}"></canvas></td>
            </tr>
            `;
            result_table_data.push(result_table_row_data);


            // matchLoc 左上角 point右下角
            cv.rectangle(src_bag_c, ele.matchLoc, ele.point, color_2, 2, cv.LINE_8, 0);// 画框

            if (result_table_row_data.order < 10) {
                // 1-9 1个文字
                let point = new cv.Point(16 + ele.matchLoc.x, ele.matchLoc.y + 32);
                cv.putText(src_bag_c, result_table_row_data.order + "", point, cv.FONT_HERSHEY_SIMPLEX, 1, color_3, 3, cv.LINE_8);// 标序号

            } else {
                // 9-？ 两个文字
                let point = new cv.Point(8 + ele.matchLoc.x, ele.matchLoc.y + 32);
                cv.putText(src_bag_c, result_table_row_data.order + "", point, cv.FONT_HERSHEY_SIMPLEX, 1, color_3, 3, cv.LINE_8);// 标序号
            }

        }

        rst_tb_dom.innerHTML = tb_innerhtml;// 生成table row dom

        cv.imshow('bag_rune_marked_Canvas', src_bag_c);
        // console.log("为Runes绘制Rect的总个数:", result_table_row_data.order);
        console.log("为Runes绘制Rect的总个数:", runes_def_fix.length);

        // cv.imshow("rune_num_marked_Canvas", scaleSearchResult1.ROI_img_s); // 匹配区域上显示



        // ----- 缩放模板匹配数字
        console.log(6);

        /**
         * @todo 缩放模板匹配符石太慢了，换 轮廓+mmCompare ，整个代码改动太大了，重写
         */

        // 缩放模板匹配数字不准确，使用轮廓+mmCompare
        if (false) {
            let canvas_num_templ = document.getElementById('templateNumCanvasInput');
            let ctx_c_n_t = canvas_num_templ.getContext('2d', {
                willReadFrequently: true,
            });

            /**
             * @deprecated 不用它，Rune缩放模板匹配误差x数字缩放模板误差 太大了 
             * */
            let scale_matched_num_sus_i_1st = undefined;

            if (runes_def_fix.length > 0) {
                for (let m = 0; m < runes_def_fix.length; m++) {
                    // if (TUNE_TOOLS_ISDEBUG && m == DEBUG_RUNES_NUM) {
                    //     break;
                    // }

                    // ----- 匹配
                    let rune = runes_def_fix[m];
                    let src_num = rune.area_o; // Rune区域，待查询
                    let src_num_c = rune.area_o.clone(); //

                    let src_num_c1 = rune.area_o.clone(); // 另外clone一份 用作表格数据展示
                    cv.imshow(result_table_data[m].canvas1, src_num_c1);

                    /**@type {Map<string,MatchedData>} */
                    let nums_matched_result = new Map(); // 存放数字匹配数据


                    for (let n = 0; n < NUMS_DATA.length; n++) {
                        if (n < DEF_NUMS_AREA[0]) {
                            continue;
                        }
                        if (n > DEF_NUMS_AREA[1]) {
                            break;
                        }
                        let num = NUMS_DATA[n];

                        /**@type {MatchedData} */
                        let nums_matched_data = {};
                        let img_num = num_imgs.get(num.name);
                        if (img_num === undefined) {
                            console.warn(`数字 ${num.name} 图片加载失败`);
                            continue;
                        }
                        ctx_c_n_t.clearRect(0, 0, canvas_num_templ.width, canvas_num_templ.height); // 清空画布
                        ctx_c_n_t.drawImage(img_num, 0, 0);
                        let templ = cv.imread('templateNumCanvasInput'); // 数字模板

                        let dst = new cv.Mat();
                        let mask = new cv.Mat();

                        // -------二值化待搜索图片与模板图片，提高缩放模板查询精准度
                        let arr_l = [240, 0, 0, 255];
                        let arr_h = [255, 255, 255, 255];

                        // 二值化待搜索图片
                        let src_b_w = new cv.Mat();// 用于保存二值化图片
                        bwImage(src_num_c, arr_l, arr_h, src_b_w);

                        // 转化数据 cv.CV_8U->cv.CV_8UC4
                        cv.imshow("numCanvas", src_b_w);
                        src_b_w = cv.imread("numCanvas"); // 二值化图
                        cv.imshow("numCanvas", src_num_c); // 最后显示的还是正常的图片

                        // 二值化模板图片
                        let templ_b_w = new cv.Mat();// 用于保存二值化图片
                        bwImage(templ, arr_l, arr_h, templ_b_w);

                        // 转化数据 cv.CV_8U->cv.CV_8UC4
                        cv.imshow("templateNumCanvasInput", templ_b_w);
                        templ_b_w = cv.imread("templateNumCanvasInput"); // 二值化图
                        cv.imshow("templateNumCanvasInput", templ); // 最后显示的还是正常的图片

                        /** @type {scaleSearchData} */
                        let scaleSearchData = {
                            defNum: true, // 识别数字
                            // defRune: true, // 识别rune
                        };
                        if (scale_matched_num_sus_i_1st !== undefined) {
                            scaleSearchData["scale_matched_num_sus_i_1st"] = scale_matched_num_sus_i_1st;
                        }
                        /** 数量已知在右下角 */
                        // console.log(src_b_w.cols, src_b_w.rows);
                        let numRect = new cv.Rect(src_b_w.cols / 2, src_b_w.rows / 2, src_b_w.cols / 2, src_b_w.rows / 2);
                        /** @todo 除了裁剪图片还可以试试canvas rect填充右上角的等级文字 */
                        let num_src_b_w = src_b_w.roi(numRect).clone();

                        // let scaleSearchResult1 = scaleSearch(src_b_w, templ_b_w, dst, mask, scaleSearchData);
                        let scaleSearchResult1 = scaleSearch(num_src_b_w, templ_b_w, dst, mask, scaleSearchData);


                        if (scaleSearchResult1.iDiffNum_res == DEFAULT_D_RES) {
                            console.warn(`Num-${num.name}没匹配到……`);
                        } else {
                            scale_matched_num_sus_i_1st = scaleSearchResult1.const_i;


                            let matchLoc = scaleSearchResult1.match_rect_data_loc;
                            let point = scaleSearchResult1.match_rect_data_point;


                            let match_rect = _Rect(matchLoc, point);
                            console.log(`识别Num-${num.name}结果:`, `i: ${scaleSearchResult1.const_i}`,
                                `iDiffNum: ${scaleSearchResult1.iDiffNum_res}`, scaleSearchResult1.size_res, match_rect);

                            // ------存储缩放模板匹配结果
                            nums_matched_data.rect = match_rect;
                            // nums_matched_data.templ = templ;// 模板数据
                            nums_matched_data.area_o = src_num.roi(match_rect);// 模板匹配成功区域(拿原图，非二值化，用克隆前的图片)
                            nums_matched_data.templ = templ_b_w;// 模板数据(二值化)
                            // nums_matched_data.area = src_b_w.roi(match_rect);// 模板匹配成功区域(二值化)
                            nums_matched_data.area = num_src_b_w.roi(match_rect);// 模板匹配成功区域(二值化)
                            nums_matched_data.checkState = CHECK_STATE.INIT;//设置初始校验状态
                            nums_matched_data.matchLoc = matchLoc;
                            nums_matched_data.point = point;



                            nums_matched_result.set(num.name, nums_matched_data);

                        }

                        templ.delete();
                        src_b_w.delete();
                        // templ_b_w.delete(); // MatchedData.templ引用，最下面清
                        dst.delete();
                        mask.delete();

                    }



                    // -----用mmCompare识别数字
                    console.log(62);
                    if (nums_matched_result.size > 0) {
                        let arr = Array.from(nums_matched_result).map((el) => {
                            let key = el[0];
                            let val = el[1];
                            val.name = key;
                            return val
                        });// map转化成数组方便操作

                        // 寻找正确的那个,两两比较
                        let max = 0;
                        let max_x = 0;
                        let max_y = 0;
                        for (let x = 0; x < NUMS_DATA.length; x++) {
                            if (x < DEF_NUMS_AREA[0]) {
                                continue;
                            }
                            if (x > DEF_NUMS_AREA[1]) {
                                break;
                            }
                            let num = NUMS_DATA[x];
                            let el = nums_matched_result.get(num.name);

                            for (let y = 0; y < arr.length; y++) {
                                let ele = arr[y];
                                let val = mmCompare(el.templ, ele.area);
                                if (val > max) {
                                    max = val;
                                    max_x = x;
                                    max_y = y;
                                }
                            }
                        }
                        console.log(`Rune-${rune.name},Num-${max_x}匹配度最高: `, max);


                        console.log(63);
                        // -----绘制矩形选框
                        let color = new cv.Scalar(0, 0, 255, 255);
                        let ele = nums_matched_result.get(max_x.toString());
                        if (!ele) {
                            console.warn("数据异常", nums_matched_result);
                        } else {

                            // cv.rectangle(src_num_c, ele.matchLoc, ele.point, color, 1, cv.LINE_8, 0);
                            // 因为裁剪了图片的右下角，又想在原图上绘制矩形选框，起始坐标为原图中心点
                            cv.rectangle(src_num_c, { x: ele.matchLoc.x + src_num_c.cols / 2, y: ele.matchLoc.y + src_num_c.rows / 2 }, { x: ele.point.x + src_num_c.cols / 2, y: ele.point.y + src_num_c.rows / 2 }, color, 1, cv.LINE_8, 0);

                            cv.imshow('rune_num_marked_Canvas', src_num_c);
                            cv.imshow("numCanvas", ele.area_o);

                            cv.imshow(result_table_data[m].canvas2, src_num_c);// 表格展示数量匹配结果
                            result_table_data[m].num = max_x;// 表格展示匹配数量值

                            rst_tb_dom.children[m].children[5].innerHTML = result_table_data[m].num;// 更改对应表格单元格数据
                        }
                    }

                    // 释放内存
                    nums_matched_result.forEach((value, key) => {
                        value.templ && value.templ.delete();
                        value.area && value.area.delete();
                        value.area_o && value.area_o.delete();
                    });
                    nums_matched_result.clear();
                    src_num_c.delete();
                    src_num_c1.delete();
                }
            }
        }

        // ----- 轮廓匹配数字
        if (runes_def_fix.length > 0) {
            // ----模板图片导入与预处理
            let ele_templ_list = document.getElementById("templ_list");
            let content = "";


            let color1 = new cv.Scalar(0, 255, 255, 255); // 轮廓颜色（天蓝色）
            let color2 = new cv.Scalar(3, 255, 32, 255); // 外接矩形颜色（数字框颜色）

            for (let n = 0; n < NUMS_DATA.length; n++) {

                if (n < DEF_NUMS_AREA[0]) {
                    continue;
                }
                if (n > DEF_NUMS_AREA[1]) {
                    break;
                }
                if (n == 0 || n == 8) {
                    continue; // 0和8暂时不考虑
                }
                let num = NUMS_DATA[n];
                let img_num = num_imgs.get(num.name);
                if (img_num === undefined) {
                    console.warn(`num ${num.name} 图片加载失败`);
                    continue;
                }
                // console.log(img_num.width, img_num.height);
                content +=
                    `<canvas id="${'num_' + num.name}" width="${img_num.width}" height="${img_num.height}"></canvas>`;
            }
            ele_templ_list.innerHTML = content;// 创建canvas


            for (let n = 0; n < NUMS_DATA.length; n++) {
                if (n < DEF_NUMS_AREA[0]) {
                    continue;
                }
                if (n > DEF_NUMS_AREA[1]) {
                    break;
                }
                if (n == 0 || n == 8) {
                    continue; // 0和8暂时不考虑 @todo待优化
                }
                let num = NUMS_DATA[n];
                let img_num = num_imgs.get(num.name);
                if (img_num === undefined) {
                    console.warn(`num ${num.name} 图片加载失败`);
                    continue;
                }

                let canvas_id = 'num_' + num.name;
                let canvas = document.getElementById(canvas_id);
                let ctx = canvas.getContext('2d', {
                    willReadFrequently: true,
                });
                ctx.drawImage(img_num, 0, 0);// 导入图片
                let templ = cv.imread(canvas_id); // num_x模板

                cv.cvtColor(templ, templ, cv.COLOR_RGBA2GRAY, 0); //转化为单通道灰度图
                cv.threshold(templ, templ, 220, 255, cv.THRESH_BINARY); // 3 thresh 阈值，4：maxval最大值


                // 模板轮廓绘制
                let contours = new cv.MatVector();
                let hierarchy = new cv.Mat();
                let dst = cv.Mat.zeros(templ.cols, templ.rows, cv.CV_8UC3);
                cv.findContours(templ, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE);

                let contours_order = 0;
                /**
                 * @todo 数字0和8轮廓要特殊处理，这里不考虑了（游戏中背包符石数量很难达到8（更别说10了），而且没有0这种情况），如果真想处理可以使用cv.minMaxLoc ，遍历所有轮廓，取出四个rect的顶点。
                 */
                cv.drawContours(dst, contours, contours_order, color1, 1, cv.LINE_8, hierarchy,
                    100); // 轮廓绘制
                // 外接矩形绘制
                let cnt = contours.get(contours_order);
                let rect_t = cv.boundingRect(cnt);
                // let point1 = new cv.Point(rect_t.x, rect_t.y);
                // let point2 = new cv.Point(rect_t.x + rect_t.width, rect_t.y + rect_t.height);
                // cv.rectangle(dst, point1, point2, color2, 1, cv.LINE_AA, 0);

                // cv.imshow(canvas_id, templ);
                // console.log(rect);
                dst = dst.roi(rect_t);
                // cv.resize(dst, dst, new cv.Size(rect_t.width, rect_t.height)); // 缩放模板图片

                cv.imshow(canvas_id, dst);


                contours.delete();
                hierarchy.delete();
                cnt.delete();
                templ.delete();
                dst.delete();
            }


            // ----识别数字
            for (let m = 0; m < runes_def_fix.length; m++) {
                // if (TUNE_TOOLS_ISDEBUG && m == DEBUG_RUNES_NUM) {
                //     break;
                // }

                let rune = runes_def_fix[m];
                let src_num = rune.area_o; // Rune区域，待查询
                let src_num_c = src_num.clone(); //

                let src_num_c1 = src_num.clone(); // 另外clone一份 用作表格数据展示
                let src_num_c2 = src_num.clone(); // 另外clone一份 用作表格数据展示
                cv.imshow(result_table_data[m].canvas1, src_num_c1);

                let arr_l = [240, 0, 0, 255];
                let arr_h = [255, 255, 255, 255];

                // 二值化待搜索图片
                let src_b_w = new cv.Mat();// 用于保存二值化图片
                bwImage(src_num_c, arr_l, arr_h, src_b_w);

                // 转化数据 cv.CV_8U->cv.CV_8UC4
                cv.imshow("numCanvas", src_b_w);
                src_b_w = cv.imread("numCanvas"); // 二值化图
                cv.imshow("numCanvas", src_num_c); // 最后显示的还是正常的图片

                // cv.imshow(result_table_data[m].canvas2_t, src_b_w);

                /** 数量已知在右下角 */
                // console.log(src_b_w.cols, src_b_w.rows);
                let numRect = new cv.Rect(src_b_w.cols / 2, src_b_w.rows / 2, src_b_w.cols / 2, src_b_w.rows / 2);
                /** @todo 除了裁剪图片还可以试试canvas rect填充右上角的等级文字 */
                let num_src_b_w = src_b_w.roi(numRect).clone();
                // cv.imshow(result_table_data[m].canvas2_t, num_src_b_w);


                let contours = new cv.MatVector();
                let hierarchy = new cv.Mat();
                let dst = cv.Mat.zeros(num_src_b_w.cols, num_src_b_w.rows, cv.CV_8UC3);
                // let dst = num_src_b_w.clone();
                // 将三通道图像（彩色图）转化为单通道图（灰度图）；第三个参数flag 转换模式 cv2.COLOR_RGB2GRAY 彩色转灰度 cv2.COLOR_GRAY2RGB 单通道转多通道
                cv.cvtColor(num_src_b_w, num_src_b_w, cv.COLOR_RGBA2GRAY, 0);
                cv.threshold(num_src_b_w, num_src_b_w, 220, 255, cv.THRESH_BINARY);

                let step_ncm_error = false;// 标识这步数字轮廓匹配是否异常
                try {
                    // if (m % 2 == 0)
                    //     throw new Error("模拟轮廓匹配待识别数字错误");
                    // cv.imshow(result_table_data[m].canvas2_t, num_src_b_w);
                    cv.findContours(num_src_b_w, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE);

                    /** 
                     * @todo 这个写死可能会有问题，比如符石匹配异常，游戏截图时有系统提示/浮屏应用遮挡；造成图片复杂，轮廓数量差异，以至于轮廓顺序差异 
                     * @todo #7367 截图分辨率不一致时这边就出问题了
                    */
                    let contours_order = 1;// screenshot1
                    // let contours_order = 0;// screenshot2 
                    // 外接矩形绘制
                    // for (let i = 0; i < contours.size(); ++i) {
                    // cv.drawContours(src_num_c, contours, contours_order, color1, 1, cv.LINE_8, hierarchy, 100); // 轮廓绘制
                    cv.drawContours(dst, contours, contours_order, color1, 1, cv.LINE_8, hierarchy, 100); // 轮廓绘制
                    // cv.imshow(result_table_data[m].canvas2_t, dst);
                    let cnt = contours.get(contours_order);

                    // cv.drawContours(dst, contours, i, color1, 1, cv.LINE_8, hierarchy, 100); // 轮廓绘制
                    // let cnt = contours.get(i);
                    let rect = cv.boundingRect(cnt);
                    // let point1 = new cv.Point(rect.x, rect.y);
                    // let point2 = new cv.Point(rect.x + rect.width, rect.y + rect.height);
                    let point1 = new cv.Point(rect.x + src_num_c2.cols / 2, rect.y + src_num_c2.rows / 2);
                    let point2 = new cv.Point(rect.x + rect.width + src_num_c2.cols / 2, rect.y + rect.height + src_num_c2.rows / 2);
                    cv.rectangle(src_num_c2, point1, point2, color2, 1, cv.LINE_AA, 0);
                    /**@done 原图上矩形选框颜色怪怪的, cv.Scalar是四个参数，就填三个肯定有问题呀 */
                    cv.imshow(result_table_data[m].canvas2, src_num_c2);// 表格展示数量匹配结果

                    let dst_c = dst.clone();
                    cv.rectangle(dst, new cv.Point(rect.x, rect.y), new cv.Point(rect.x + rect.width, rect.y + rect.height), color2, 1, cv.LINE_AA, 0);
                    // cv.imshow(result_table_data[m].canvas2_t, dst);
                    dst = dst_c.roi(rect);
                    cv.imshow(result_table_data[m].canvas2_t, dst);
                    cnt.delete();

                    // cv.imshow(result_table_data[m].canvas2_t, dst);
                    // }
                    // cv.imshow(result_table_data[m].canvas2_t, dst);
                } catch (error) {
                    step_ncm_error = true;
                    console.error("轮廓匹配待识别数字异常", error);
                }
                let similarity = 0;
                let result_num = -1;
                // ----用mmCompare识别数字
                try {
                    if (step_ncm_error) {
                        throw new Error("模拟数字识别错误");
                    }
                    for (let n = 0; n < NUMS_DATA.length; n++) {
                        if (n < DEF_NUMS_AREA[0]) {
                            continue;
                        }
                        if (n > DEF_NUMS_AREA[1]) {
                            break;
                        }
                        if (n == 0 || n == 8) {
                            continue; // 0和8暂时不考虑
                        }
                        let num = NUMS_DATA[n];
                        let img_num = num_imgs.get(num.name);
                        if (img_num === undefined) {
                            console.warn(`num ${num.name} 图片加载失败`);
                            continue;
                        }
                        let canvas_id = 'num_' + num.name;
                        let templ = cv.imread(canvas_id);
                        let dst = cv.imread(result_table_data[m].canvas2_t);
                        let res = mmCompare(dst, templ);
                        // let res = mmCompare(templ, dst);
                        if (res > similarity) {
                            similarity = res;
                            result_num = n;
                        }

                    }
                    console.log(`数字${result_num}的相似度(${similarity})最高`);
                } catch (error) {
                    console.error("用mmCompare识别数字异常", error);
                }
                // cv.imshow(result_table_data[m].canvas2, src_num_c2);// 表格展示数量匹配结果
                result_table_data[m].num = result_num;// 表格展示匹配数量值

                rst_tb_dom.children[m].children[5].innerHTML = result_table_data[m].num;// 更改对应表格单元格数据
            }
        }

        // let dom_table_wp = document.getElementsByClassName("table_wrapper")[0];
        // dom_table_wp.classList.add("show");


        // ----- 释放内存
        console.log(7);

        src_screenshot.delete();
        src_s_c.delete();

        bag_matched_data.templ && bag_matched_data.templ.delete();
        bag_matched_data.area && bag_matched_data.area.delete();
        bag_matched_data.area_o && bag_matched_data.area_o.delete();
        src_bag.delete();
        src_bag_c.delete();

        runes_matched_result.forEach((value, key) => {
            value.templ && value.templ.delete();
            value.area && value.area.delete();
            value.area_o && value.area_o.delete();
        });
        runes_matched_result.clear();

        rune_imgs.clear();
        num_imgs.clear();


        // console.timeEnd("执行时间");
        console.timeEnd("识别时间");

    };




}


/**
 * @function
 * @description 缩放模板图片并进行模板匹配
 * @param { cv.Mat }  src 
 * @param { cv.Mat }  templ 
 * @param { cv.Mat }  dst 
 * @param { cv.Mat }  mask 
 *
 * @param { scaleSearchData} [data = {}] 携带参数，默认值为{}
 * 
 * @returns { scaleSearchResult } 
 * 
 * @todo scale_matched_rune_sus_i_1st 这个值用它会不会有问题，比如第一次成功缩放匹配的那个rune其实是误识别的rune;换不同分辨率截图时这个问题已经出现了（主要是模板图片放大到最大也没有实际截图中的大），调整start_i,end_i,增大缩放范围，已匹配时长的代价换取匹配精度
 * @todo 精度缩放调整，在第一次找到最好的模板匹配时，以这为起始值，左右缩放（微调），默认缩放比率 0.05，注意不要随意改0.05，改它的话要考虑循环次数
 * @todo 考虑new cv.Rect(object,cv.Point)得到的rect ，其width,height 为undefined的问题 用_Rect是否有隐患
 * @todo 研究pHash函数，以及考虑它是否在scaleSearch中使用有没有错误的可能 iDiffNum有啥用
 * @todo 研究dst.convertTo(dst, cv.CV_8UC4); 为啥模板匹配用cv.imread的数据就没问题
 */
function scaleSearch(src, templ, dst, mask, data = {}) {
    // We use the function: cv.matchTemplate (image, templ, result, method, mask = new cv.Mat())

    // Parameters
    // image	image where the search is running. It must be 8-bit or 32-bit floating-point.
    // templ	searched template. It must be not greater than the source image and have the same data type.
    // result	map of comparison results. It must be single-channel 32-bit floating-point.
    // method	parameter specifying the comparison method(see cv.TemplateMatchModes).
    // mask	mask of searched template. It must have the same datatype and size with templ. It is not set by default.
    // console.log(cv.Size);

    // -----循环缩放模板图片

    let iDiffNum_res = DEFAULT_D_RES; // iDiffNum值越小，对应越相似的结果
    let size_res = null;
    let const_i; // 记录第一次成功缩放匹配时的i值，方便调试


    // 用于保存matchLoc，point ,将画框操作与scaleSearch解耦
    let match_rect_data_loc;
    let match_rect_data_point;

    let isDefBag = (data.defBag !== undefined);
    let isDefRune = (data.defRune !== undefined);
    let isDefNum = (data.defNum !== undefined);

    let start_i = 0;
    let end_i = 10;

    if (isDefBag) {
        start_i = -10;
        end_i = 10;

    } else if (isDefRune) {
        // screenshot1
        // start_i = -10;
        // end_i = 0;

        // screenshot2
        start_i = -20;
        end_i = 20;

    } else if (isDefNum) {
        // screenshot1
        // start_i = 0;
        // end_i = 15;

        // screenshot2
        start_i = -10;
        end_i = 15;
    }

    // for (let i = 0; i < 20; i++) { // num
    // for (let i = -10; i < 20; i++) {
    // for (let i = -10; i < 0; i++) { // rune
    for (let i = start_i; i < end_i; i++) {
        // i> 0 放大， i<0 缩小
        // if (isDefRune && i !== -9) { // screenshot1 上一次成功运行rune后保存的const_i值方便调试
        if (isDefBag) {
            if (TUNE_TOOLS_ISDEBUG) {
                if (i !== DEBUG_BAG_I) {
                    continue;
                }
            }
        } else if (isDefRune) {
            if (TUNE_TOOLS_ISDEBUG) {
                if (i !== DEBUG_RUNE_I) {
                    continue;
                }
                data.scale_matched_rune_sus_i_1st = DEBUG_RUNE_I;
            }

            if (data.scale_matched_rune_sus_i_1st !== undefined) {
                if (i !== data.scale_matched_rune_sus_i_1st) {// 循环优化
                    continue;
                }
            }
        } else if (isDefNum) {
            if (TUNE_TOOLS_ISDEBUG) {
                // if (i !== DEBUG_NUM_I) {
                //     continue;
                // }
                // data.scale_matched_num_sus_i_1st = DEBUG_NUM_I;


                // if (data.scale_matched_num_sus_i_1st !== undefined) {
                //     if (i !== data.scale_matched_num_sus_i_1st) {// 循环优化
                //         continue;
                //     }
                // }
            }

            // 考虑到rune可能识别错误，就不优化它了
            // if (data.scale_matched_num_sus_i_1st !== undefined) {
            //     if (i !== data.scale_matched_num_sus_i_1st) {// 循环优化
            //         continue;
            //     }
            // }
        }

        /**
         * @todo #7367 目前不同案例是通过改 start_i，end_i ， threshold_num，不灵活。
         * 解决思路 按导入 截图尺寸 先预估出（符石、数字） 大小，在 开始微调缩放
         */
        let templ_c = templ.clone();
        let col = templ.cols - i * 0.05 * templ.cols;
        let row = templ.rows - i * 0.05 * templ.rows;
        // if (isDefRune) {
        //     console.log("缩放处理符石i:",i );
        // }
        if (col > src.cols || row > src.rows) {
            // if (isDefRune) {
            //     console.log("模板图片尺寸不能比待搜索图片大! i=",i);
            // }
            continue;// 模板图片尺寸不能比待搜索图片大
        }
        // console.log(col,row);
        // console.log(cv.resize);
        // console.log(cv.size);
        // console.log(new cv.Size(col, row));
        let size = new cv.Size(col, row);
        // let size = new cv.Size(Math.ceil(col), Math.ceil(row));
        cv.resize(templ_c, templ_c, size);


        // -----模板匹配
        // cv.matchTemplate(src, templ_c, dst, cv.TM_CCOEFF_NORMED, mask);
        cv.matchTemplate(src, templ_c, dst, 0); // 0: cv.TM_SQDIFF 4:cv.TM_CCOEFF 5:cv.TM_CCOEFF_NORMED

        let result = cv.minMaxLoc(dst, mask);
        // console.log(result);
        let maxLoc = result.maxLoc;
        let minLoc = result.minLoc;
        let minVal = result.minVal || -1;
        let maxVal = result.maxVal;
        let matchLoc;

        // let ROI = new cv.Rect(minLoc.x, maxLoc.y, col, row);

        let src_c = src.clone();// （图片二值化了，最后要显示的是原图）原图上标记
        matchLoc = minLoc;

        //-----根据result中最大值位置 画出矩形和中心点
        // let center = new cv.Point(minLoc.x + templ_c.cols / 2, minLoc.y + templ_c.rows / 2);

        let point = new cv.Point(matchLoc.x + templ_c.cols, matchLoc.y + templ_c.rows);


        // console.log(cv.Rect);


        // let img_ROI = new cv.Rect(matchLoc, new cv.Point(matchLoc.x + templ_c.cols, matchLoc.y + templ_c.rows));// width,height undefined???
        // let img_ROI = new cv.Rect(matchLoc.x, matchLoc.y, templ_c.cols, templ_c.rows); // 直接这样写也得结果 但不知道会不会有问题
        let img_ROI = _Rect(matchLoc, point); // 模仿Rect_<_Tp>::Rect_
        let img = src.clone();
        // console.log(img, img_ROI);
        // let ROI_img = img(img_ROI); // img is not a function
        /***
         * Mat Mat::operator()( const Rect& roi ) const
         *   {
         *       return Mat(*this, roi);
         *   }
         *
         */
        // let ROI_img = new cv.Mat(img, img_ROI); // return Mat(*this, roi); js不支持
        let ROI_img = img.roi(img_ROI); // return Mat(*this, roi); 
        // console.log(ROI_img);
        // cv.imshow('screenshot_bag_marked_Canvas', ROI_img);

        // let match_rect = _Rect(matchLoc, point);
        // let ROI_img_original = src_c.roi(match_rect); // 比对的是二值化的图片，我们要的是原图(识别数字、显示识别区域有用)

        //-----进行相似度比较
        let iDiffNum = pHash(ROI_img, templ);
        // console.log("iDiffNum:", iDiffNum);
        // console.log(`i: ${i}`, size, `iDiffNum: ${iDiffNum}`);
        // if (pHash(ROI_img, templ) < 20) { // 1:1图片识别
        // if (iDiffNum <= 20) { // ！！！！缩放图片识别
        // if (iDiffNum <= 21) { // ！！！！rune不是纯背景色，有数字字母
        // if (iDiffNum <= 35) { // ！！！！数字处背景杂，相似度低

        let threshold_num = 21;
        if (isDefBag) {
            threshold_num = 21;
        } else if (isDefRune) {
            // threshold_num = 25;
            // threshold_num = 29;// screenshot2 BOR误识别成ODO
            threshold_num = 35;// screenshot2
        } else if (isDefNum) {
            threshold_num = 35; // ！！！！数字处背景杂，相似度低
        }

        if (iDiffNum <= threshold_num) {
            if (iDiffNum == 0) {
                iDiffNum_res = 0;
                const_i = i;
                size_res = size;

                match_rect_data_loc = matchLoc;
                match_rect_data_point = point;
                // 完全一致，直接跳出循环
                break;
            } else {
                if (iDiffNum_res > iDiffNum) {
                    iDiffNum_res = iDiffNum;
                    const_i = i;
                    size_res = size;
                    match_rect_data_loc = matchLoc;
                    match_rect_data_point = point;
                }
            }
        }

        templ_c.delete();
        src_c.delete();

        img.delete();
        ROI_img.delete();

        // console.log("^^^");
    }

    return {
        // "templ_b_w": templ_b_w,// 模板（二值化）
        // "ROI_img_b_w": ROI_img_b_w,// 选区（二值化）

        // res,
        iDiffNum_res,
        size_res,
        const_i,
        // ROI_img_s,
        match_rect_data_loc,
        match_rect_data_point
    };
}

/**
 * @function
 * @description 图片二值化处理
 * @param { cv.Mat } src 要处理的图像
 * @param { number[] } arr_l lowerb
 * @param { number[] } arr_h upperb
 * @param { cv.Mat } dst 用于输出二值化图像
 * 
 * @returns { void }
 * @link OpenCV.js 官方文档 cvtColor inRange 使用示例 https://docs.opencv.org/4.5.5/db/d64/tutorial_js_colorspaces.html
 * @todo 数据转化方法用cv.imshow,cv.imread 怪怪的，但不转模板匹配函数会报错。有时间找到正确的方法，把它放到这个函数里，让out跟scr格式一致
 * 
*/
function bwImage(src, arr_l, arr_h, dst) {
    let src_original = src.clone(); // 待查询 ↑提高变量作用域
    let low_hsv_src = new cv.Mat(src_original.rows, src_original.cols, src_original.type(), arr_l);
    let high_hsv_src = new cv.Mat(src_original.rows, src_original.cols, src_original.type(), arr_h);
    // let src_b_w = new cv.Mat(); // 保存二值化图片blank&white
    // cv.inRange(src_original, low_hsv_src, high_hsv_src, src_b_w);
    cv.inRange(src_original, low_hsv_src, high_hsv_src, dst);

    src_original.delete();

}

/**
 * @function
 * @description 求两个rect相交重叠度
 * @param { Rect } rect1
 * @param { Rect } rect2
 * 
 * @returns { float } [0,1]
 * @link https://blog.csdn.net/kezunhai/article/details/8830882
 * 
*/
function rrOverlap(rect1, rect2) {
    if (rect1.x > rect2.x + rect2.width) { return 0.0; }
    if (rect1.y > rect2.y + rect2.height) { return 0.0; }
    if (rect1.x + rect1.width < rect2.x) { return 0.0; }
    if (rect1.y + rect1.height < rect2.y) { return 0.0; }

    let colInt = Math.min(rect1.x + rect1.width, rect2.x + rect2.width) - Math.max(rect1.x, rect2.x);
    let rowInt = Math.min(rect1.y + rect1.height, rect2.y + rect2.height) - Math.max(rect1.y, rect2.y);
    let intersection = colInt * rowInt;
    let area1 = rect1.width * rect1.height;
    let area2 = rect2.width * rect2.height;
    return intersection / (area1 + area2 - intersection);
}

/***
 * @function
 * @description 两个图像相似度比较。
 * @description 最后就是模板匹配了，匹配的方式有很多种，这里采用的是对数字和模板进行像素点的匹配，根据相同像素点的百分占比来得出结论，相同像素点越多，说明匹配相似度越高，最后以匹配程度最高的作为最终识别结果
 * @param { cv.Mat } img1 img1 注意保证两个图的通道数一致
 * @param { cv.Mat } img2 img2 注意保证两个图的通道数一致
 * 
 * @returns { float } [0,1]
 * @link https://blog.csdn.net/m0_71741835/article/details/127937354
 * 
*/
function mmCompare(img1, img2) {
    if (img1.channels() !== img2.channels()) {
        console.error("img1与img2通道数不一致");
        return 0;
    }
    // console.log(img1.channels(), img2.channels());
    let my_temp = new cv.Mat();
    if (img1.cols > img2.cols) {
        // console.log("xxx");
        cv.resize(img2, my_temp, img1.size());
    } else {
        // console.log("yyy");
        cv.resize(img1, my_temp, img2.size());
    }
    let rows, cols;
    let img_point, temp_point; //像素类型uchar

    // console.log(my_temp.rows, img1.rows, img2.rows);
    // console.log(my_temp.cols, img1.cols, img2.cols);
    rows = my_temp.rows;
    cols = my_temp.cols * img2.channels();

    let result, same = 0.0,
        different = 0.0;
    for (let i = 0; i < rows; i++) //遍历图像像素
    {
        //获取像素值
        if (img1.cols > img2.cols) {
            img_point = img1.ucharPtr(i);
        } else {
            img_point = img2.ucharPtr(i);
        }
        temp_point = my_temp.ucharPtr(i);
        for (let j = 0; j < cols; j++) {
            // let test1 = img_point[j];
            // let test2 = temp_point[j];
            // console.log(test1, test2);
            if (img_point[j] == temp_point[j]) {
                same++; //记录像素相同的个数
                // if (img_point[j] !== 0) {
                //     same++;//记录像素相同的个数
                // }
            } else {
                different++; //记录像素不同的个数
            }

        }
    }
    result = same / (same + different);
    my_temp.delete();
    return result; //返回匹配结果
}


/***
 * @function
 * @description 获取匹配得到的区域;  为解决 new cv.Rect(object,cv.Point)得到的rect ，其width,height 为undefined的问题
 * @param { Loc } pt1
 * @param { cv.Point } pt2
 * 
 * @returns { cv.Rect }
 * 
 * @example
 * // 原功能Rect_
 * Rect_<_Tp>::Rect_(const Point_<_Tp>& pt1, const Point_<_Tp>& pt2)
    {
        x = std::min(pt1.x, pt2.x);
        y = std::min(pt1.y, pt2.y);
        width = std::max(pt1.x, pt2.x) - x;
        height = std::max(pt1.y, pt2.y) - y;
    }
 * 
*/
function _Rect(pt1, pt2) {
    let x = pt1.x < pt2.x ? pt1.x : pt2.x;
    let y = pt1.y < pt2.y ? pt1.y : pt2.y;
    let w = pt1.x > pt2.x ? pt1.x : pt2.x - x;
    let h = pt1.y > pt2.y ? pt1.y : pt2.y - y;
    return new cv.Rect(x, y, w, h);
}

/***
 * @function
 * @description
 * @param { cc.Mat } matSrc1
 * @param { cc.Mat }  matSrc2
 * 
 * @returns { number } iDiffNum
 * 
 * @link opencv中的模板匹配-- 2.4 自适应目标匹配 https://blog.csdn.net/qq_46418503/article/details/119675943
 * 
*/
function pHash(matSrc1, matSrc2)
//int main()
{
    let matDst1 = new cv.Mat();
    let matDst2 = new cv.Mat(); // cv.resize 1,2参数类型检查 不能是undefined

    //    let matSrc1 = cv.imread("../1.jpg");
    //    let matSrc2 = cv.imread("../3.jpg");
    // console.log(new cv.Size(32, 32), cv.INTER_CUBIC);
    cv.resize(matSrc1, matDst1, new cv.Size(32, 32), 0, 0, cv.INTER_CUBIC);
    cv.resize(matSrc2, matDst2, new cv.Size(32, 32), 0, 0, cv.INTER_CUBIC);

    // console.log(cv.CV_BGR2GRAY,cv.COLOR_BGR2GRAY);// cv.CV_BGR2GRAY 废弃
    cv.cvtColor(matDst1, matDst1, cv.COLOR_BGR2GRAY);
    cv.cvtColor(matDst2, matDst2, cv.COLOR_BGR2GRAY);

    matDst1.convertTo(matDst1, cv.CV_32F);
    matDst2.convertTo(matDst2, cv.CV_32F);

    // opencv.js不支持 dct  
    // cv.dct(matDst1, matDst1);
    // cv.dct(matDst2, matDst2);

    // ！！！！这边跟原博文不一致，换成了dft
    // Is opencv.js supports dft and dct now? #22383 https://github.com/opencv/opencv/issues/22383
    cv.dft(matDst1, matDst1);
    cv.dft(matDst2, matDst2);

    let iAvg1 = 0;
    let iAvg2 = 0;
    let arr1 = new Array(64);
    let arr2 = new Array(64);

    for (let i = 0; i < 8; i++) {
        // uchar * data1 = matDst1.ptr < uchar > (i);
        // uchar * data2 = matDst2.ptr < uchar > (i);

        // let data1 = matDst1.ptr(i);
        // let data2 = matDst2.ptr(i);

        let data1 = matDst1.ucharPtr(i);
        let data2 = matDst2.ucharPtr(i);

        // console.log(data1, data2);

        let tmp = i * 8;

        for (let j = 0; j < 8; j++) {
            let tmp1 = tmp + j;

            arr1[tmp1] = data1[j];
            arr2[tmp1] = data2[j];

            iAvg1 += arr1[tmp1];
            iAvg2 += arr2[tmp1];
        }
    }

    iAvg1 /= 64;
    iAvg2 /= 64;

    for (let i = 0; i < 64; i++) {
        arr1[i] = (arr1[i] >= iAvg1) ? 1 : 0;
        arr2[i] = (arr2[i] >= iAvg2) ? 1 : 0;
    }

    let iDiffNum = 0;

    for (let i = 0; i < 64; i++) {
        if (arr1[i] != arr2[i]) {
            ++iDiffNum;
        }
    }

    matDst1.delete();
    matDst2.delete();

    //    cout<<iDiffNum<<endl;
    return iDiffNum;
}


/***
 * @function
 * @description 加载图片资源
 * @param { string } url 图片地址
 * 
 * @returns { Promise } 
 * 
 * 
*/
function loadImg(url) {
    return new Promise((resolve, reject) => {
        let img = new Image();
        // img.crossOrigin = 'anonymous';
        // 跨域 直接用live server插件运行
        img.src = url;
        img.onload = () => {
            resolve(img);
        };
        img.onerror = reject;
    });
}
